const fs = require("fs");
const { set, unset } = require("lodash");
const decodeBase64 = require("jszip/lib/base64").decode;

function decodeBase64ToBuffer(base64) {
  const prefix = "data:image/png;base64,";
  if (base64.startsWith(prefix)) {
    base64 = base64.substring(prefix.length);
  }
  return Buffer.from(decodeBase64(base64).buffer);
}


// ----------- style metadata --------------
const colorStructure = {
  argb: 1,
  theme: 1,
};

const borderStructure = {
  style: 1,
  color: colorStructure,
};

const styleStructure = {
  numFmt: 1,
  font: {
    name: 1,
    size: 1,
    family: 1,
    scheme: 1,
    charset: 1,
    color: colorStructure,
    bold: 1,
    italic: 1,
    underline: 1,
    vertAlign: 1,
    strike: 1,
    outline: 1,
  },
  alignment: {
    horizontal: 1,
    vertical: 1,
    wrapText: 1,
    shrinkToFit: 1,
    indent: 1,
    readingOrder: 1,
    textRotation: 1,
  },
  protection: {
    locked: 1,
  },
  border: {
    top: borderStructure,
    left: borderStructure,
    bottom: borderStructure,
    right: borderStructure,
    diagonal: borderStructure,
  },
  fill: {
    type: 1,
    pattern: 1,
    fgColor: colorStructure,
    bgColor: colorStructure,
    gradient: 1,
    degree: 1,
    stops: [
      {
        position: 1,
        color: colorStructure,
      },
    ],
    center: {
      left: 1,
      top: 1,
    },
  },
};

// ----------- style metadata --------------

class CloneContext {
  constructor() {
    this.varName = 0;
    this.functionName = 0;
    this.cloneHelper = {};
  }

  get nextVar() {
    return `var_${this.varName++}`;
  }

  get nextFunction() {
    return `func_${this.functionName++}`;
  }

  defineFunction(body) {
    const funcName = this.nextFunction;
    this.cloneHelper[funcName] = new Function("context", "input", body);
    return `context['${funcName}']`;
  }
}

function makeCloneObject(context, metadata) {
  const codes = ["if (input == null) return input", "const result = {}"];
  for (const [key, value] of Object.entries(metadata)) {
    codes.push(`if (input['${key}'] != null) {`);
    if (value == null || typeof value !== "object") {
      codes.push(`result['${key}'] = input['${key}']`);
    } else {
      let maker;
      if (Array.isArray(value)) {
        maker = makeCloneArray;
      } else {
        maker = makeCloneObject;
      }
      const funcName = context.defineFunction(maker(context, value));
      codes.push(`result['${key}'] = ${funcName}(context, input['${key}'])`);
    }
    codes.push("}");
  }
  codes.push("return result");
  return codes.join("\n");
}

function makeCloneArray(context, metadata) {
  metadata = metadata[0];
  if (metadata == null || typeof metadata !== "object") {
    return `return Array.isArray(input) ? [...input] : input`;
  }
  let functionBody;
  if (Array.isArray(metadata)) {
    functionBody = makeCloneArray(context, metadata);
  } else {
    functionBody = makeCloneObject(context, metadata);
  }
  const functionName = context.defineFunction(functionBody);
  return `
if (!Array.isArray(input)) throw new Error('字段类型必须是数组')
const res = []
for (const item of input) {
res.push(${functionName}(context, item))
}
return res`;
}

function createNullSafeObject(
  proxyCache,
  root,
  currentObj,
  pathPrefix,
  metadata
) {
  return new Proxy(
    { proxyCache, root, currentObj, pathPrefix, metadata },
    {
      get(target, p) {
        const { root, currentObj, metadata, pathPrefix } = target;
        const value = currentObj ? currentObj[p] : undefined;
        let valueMetadata;

        if (Array.isArray(metadata) && typeof p === "string" && /\d+/.test(p)) {
          // 数组下标访问
          valueMetadata = metadata[0];
        } else {
          valueMetadata = metadata[p];
        }

        if (typeof value === "function") {
          // 访问函数则bind后返回
          return value.bind(currentObj);
        }

        // 访问不存在、基础类型的字段，直接返回
        if (!valueMetadata || typeof valueMetadata !== "object") {
          return value;
        }

        const nextPathPrefix = [...pathPrefix, p];
        const hash = nextPathPrefix.join("");
        let nullSafeProxy = proxyCache[hash];
        if (!nullSafeProxy) {
          nullSafeProxy = createNullSafeObject(
            proxyCache,
            root,
            value,
            nextPathPrefix,
            valueMetadata
          );
          proxyCache[hash] = nullSafeProxy;
        }

        return nullSafeProxy;
      },

      set(target, p, newValue) {
        const { root, pathPrefix } = target;
        return set(root, [...pathPrefix, p], newValue);
      },

      deleteProperty(target, p) {
        const { root } = target;
        return unset(root, [...pathPrefix, p]);
      },
    }
  );
}

// useful stuff
const inherits = function (cls, superCtor, statics, prototype) {
  // eslint-disable-next-line no-underscore-dangle
  cls.super_ = superCtor;

  if (!prototype) {
    prototype = statics;
    statics = null;
  }

  if (statics) {
    Object.keys(statics).forEach((i) => {
      Object.defineProperty(
        cls,
        i,
        Object.getOwnPropertyDescriptor(statics, i)
      );
    });
  }

  const properties = {
    constructor: {
      value: cls,
      enumerable: false,
      writable: false,
      configurable: true,
    },
  };
  if (prototype) {
    Object.keys(prototype).forEach((i) => {
      properties[i] = Object.getOwnPropertyDescriptor(prototype, i);
    });
  }

  cls.prototype = Object.create(superCtor.prototype, properties);
};

// eslint-disable-next-line no-control-regex
const xmlDecodeRegex = /[<>&'"\x7F\x00-\x08\x0B-\x0C\x0E-\x1F]/;
const utils = {
  nop() {},
  promiseImmediate(value) {
    return new Promise((resolve) => {
      if (global.setImmediate) {
        setImmediate(() => {
          resolve(value);
        });
      } else {
        // poorman's setImmediate - must wait at least 1ms
        setTimeout(() => {
          resolve(value);
        }, 1);
      }
    });
  },
  inherits,
  dateToExcel(d, date1904) {
    return 25569 + d.getTime() / (24 * 3600 * 1000) - (date1904 ? 1462 : 0);
  },
  excelToDate(v, date1904) {
    const millisecondSinceEpoch = Math.round(
      (v - 25569 + (date1904 ? 1462 : 0)) * 24 * 3600 * 1000
    );
    return new Date(millisecondSinceEpoch);
  },
  parsePath(filepath) {
    const last = filepath.lastIndexOf("/");
    return {
      path: filepath.substring(0, last),
      name: filepath.substring(last + 1),
    };
  },
  getRelsPath(filepath) {
    const path = utils.parsePath(filepath);
    return `${path.path}/_rels/${path.name}.rels`;
  },
  xmlEncode(text) {
    const regexResult = xmlDecodeRegex.exec(text);
    if (!regexResult) return text;

    let result = "";
    let escape = "";
    let lastIndex = 0;
    let i = regexResult.index;
    for (; i < text.length; i++) {
      const charCode = text.charCodeAt(i);
      switch (charCode) {
        case 34: // "
          escape = "&quot;";
          break;
        case 38: // &
          escape = "&amp;";
          break;
        case 39: // '
          escape = "&apos;";
          break;
        case 60: // <
          escape = "&lt;";
          break;
        case 62: // >
          escape = "&gt;";
          break;
        case 127:
          escape = "";
          break;
        default: {
          if (
            charCode <= 31 &&
            (charCode <= 8 || (charCode >= 11 && charCode !== 13))
          ) {
            escape = "";
            break;
          }
          continue;
        }
      }
      if (lastIndex !== i) result += text.substring(lastIndex, i);
      lastIndex = i + 1;
      if (escape) result += escape;
    }
    if (lastIndex !== i) return result + text.substring(lastIndex, i);
    return result;
  },
  xmlDecode(text) {
    return text.replace(/&([a-z]*);/g, (c) => {
      switch (c) {
        case "&lt;":
          return "<";
        case "&gt;":
          return ">";
        case "&amp;":
          return "&";
        case "&apos;":
          return "'";
        case "&quot;":
          return '"';
        default:
          return c;
      }
    });
  },
  validInt(value) {
    const i = parseInt(value, 10);
    return !Number.isNaN(i) ? i : 0;
  },

  isDateFmt(fmt) {
    if (!fmt) {
      return false;
    }

    // must remove all chars inside quotes and []
    fmt = fmt.replace(/\[[^\]]*]/g, "");
    fmt = fmt.replace(/"[^"]*"/g, "");
    // then check for date formatting chars
    const result = fmt.match(/[ymdhMsb]+/) !== null;
    return result;
  },

  fs: {
    exists(path) {
      return new Promise((resolve) => {
        fs.access(path, fs.constants.F_OK, (err) => {
          resolve(!err);
        });
      });
    },
  },

  toIsoDateString(dt) {
    return dt.toISOString().substring(0, 10);
  },

  parseBoolean(value) {
    return value === true || value === "true" || value === 1 || value === "1";
  },

  makeClone(metadata) {
    const context = new CloneContext();
    let functionBody;
    // 基础类型字段直接复制；object、array生成对应的clone函数后，迭代复制
    if (metadata == null || typeof metadata !== "object") {
      functionBody = "return input";
    } else {
      if (Array.isArray(metadata)) {
        functionBody = makeCloneArray(context, metadata);
      } else {
        functionBody = makeCloneObject(context, metadata);
      }
    }
    const func = new Function("context", "input", functionBody);
    return function (input) {
      return func(context.cloneHelper, input);
    };
  },

  createNullSafeObject(root, metadata) {
    return createNullSafeObject({}, root, root, [], metadata);
  },

  ridGenerator(start = 1) {
    return function () {
      return `rId${start++}`;
    };
  },

  styleStructure,

  isInternalLink(model) {
    // @example: Sheet2!D3, return true
    return model.isInternal === true || (model.target && /^[^!]+![a-zA-Z]+\d+$/.test(model.target));
  },

  decodeBase64ToBuffer
};

utils.copyStyle = utils.makeClone(utils.styleStructure);

module.exports = utils;
